Lurch Deductive Engine

Tutorial: LC tree hierarchies

LC tree hierarchies

This tutorial assumes you know how to construct LC instances; if not, see the tutorial on Constructing LCs. This tutorial covers the essential read/write operations available on a hierarchy of nested LC structures.

Every method documented here is actually defined not in the LogicConcept class, but in the MathConcept class, which is a more general concept than an LC. Once interpretation of syntax has been built into the LDE, we will use it to "interpret" MathConcepts into LogicConcepts. Although that feature of the LDE has not yet been built, the MathConcept class remains the most general class from which most others inherit.

(Each piece of sample code below is written as if it were a script sitting in the root folder of this source code repository, and run from there with the command-line tools node. If you place your scripts in another folder, you will need to adjust the path in each import statement accordingly. If you have not yet set up a copy of this repository with the appropriate Node.js version installed, see our GitHub README, which explains how to do so.)

Preparing by building some LCs

Let's build a few nested LCs, so that we can use them as recurring examples in the code below.



import { LogicConcept } from './src/index.js'

const famous = LogicConcept.fromPutdown( '(= (+ (^ e (* i pi)) 1) 0)' )[0]
const env = LogicConcept.fromPutdown( '{ :A B :C D :E F }' )[0]


Reading parent, child, and sibling relationships

Parents and children can be looked up in the ways you would expect.



const lhs = famous.child( 1 )
const rhs = famous.child( 2 )
console.log( 'LHS:', lhs.toPutdown(), 'RHS:', rhs.toPutdown() )
console.log( 'LHS parent is whole equation?', lhs.parent() === famous )
console.log( 'Operator and both sides:', famous.children().map( x => x.toPutdown() ) )
console.log( 'In total, this many children:', famous.numChildren() )
// Console output:
LHS: (+ (^ e (* i pi)) 1) RHS: 0
LHS parent is whole equation? true
Operator and both sides: [ '=', '(+ (^ e (* i pi)) 1)', '0' ]
In total, this many children: 3


There are also convenience functions for getting the first or last child, or for getting the array of children without the first or last child.



console.log( env.firstChild().toPutdown() )
console.log( env.lastChild().toPutdown() )
console.log( env.allButFirstChild().map( x => x.toPutdown() ) )
console.log( env.allButLastChild().map( x => x.toPutdown() ) )
// Console output:
:A
F
[ 'B', ':C', 'D', ':E', 'F' ]
[ ':A', 'B', ':C', 'D', ':E' ]


You can also ask for the next or previous sibling (child of same parent).



const C = env.child( 2 )
console.log( 'Third child:', C.toPutdown() )
console.log( 'Right before it:', C.previousSibling().toPutdown() )
console.log( 'Right after it:', C.nextSibling().toPutdown() )
// Console output:
Third child: :C
Right before it: B
Right after it: D


Reading more deeply: descendants and ancestors

Parent and child relationships are just one step up or down the LC hierarchy. We can represent several steps down into children of children using an array of child indices called an address. We look up descendants by address using a method called index().



const pi = famous.index( [ 1, 1, 2, 2 ] )
console.log( 'Name of that descendant:', pi.toPutdown() )
console.log( 'Address in the equation:', pi.address() )
// Console output:
Name of that descendant: pi
Address in the equation: [ 1, 1, 2, 2 ]


We can also get a list of all the ancestors of a descendant LC, in order from bottom upwards.



console.log( pi.ancestors().map( x => x.toPutdown() ) )
// Console output:
[
  'pi',
  '(* i pi)',
  '(^ e (* i pi))',
  '(+ (^ e (* i pi)) 1)',
  '(= (+ (^ e (* i pi)) 1) 0)'
]


Filtering for only some results

It's also common to want to find just those children, descendants, or ancestors satisfying a certain condition (whatever that condition may be). There are therefore functions childrenSatisfying(), descendantsSatisfying(), and ancestorsSatisfying(), which take a unary predicate on LCs as their argument and return results filtered using that predicate. Here is one example.



// Get every descendants of the famous equation that is atomic.
const leaves = famous.descendantsSatisfying( x => x.isAtomic() )
console.log( leaves.map( x => x.toPutdown() ) )
// Console output:
[
  '=',  '+', '^',
  'e',  '*', 'i',
  'pi', '1', '0'
]


Sometimes, you just want to check to see if there exists a child, descendant, or ancestor with the given property, rather than fetch all of them. (It is usually more efficient this way, because you can stop looking when you've found one, rather than continuing to find all of them and assemble the results into an array.) There are thus corresponding methods hasChildSatisfying(), hasDescendantSatisfying(), and hasAncestorSatisfying(), which take the same unary predicate and return a boolean.

Altering the list of children of an LC

You can insert and remove children using zero-based indexing. This modifies the original LC in place; it does not create a copy.



console.log( 'Before:', env.toPutdown() )
env.removeChild( 2 )
env.insertChild( new LurchSymbol( 'YO' ), 0 )
console.log( 'After:', env.toPutdown() )
// Console output:
Before: { :A B :C D :E F }
After: { YO :A B D :E F }


Because one often wants to insert or remove a child from the beginning or ending of the list of children, there are four methods whose names correspond to the methods of the same names on JavaScript arrays: pushChild(), popChild(), shiftChild(), and unshiftChild().

You can also replace all the children at once with a call to setChildren(), which takes an array of any length as argument; it need not be the same number of children.

Warning: moving an LC to a new location removes it from its old location!

If you use any of the tools described above for inserting a new child into an LC, if that child was formerly a child of a different LC, it will be removed from its original location before being placed in the new location! This can sometimes be surprising, if you forget that each LC can exist in only one location in one LC tree at a time.

Consider the following example. The goal is to write the equation $6x-5=6x-5$, but using the wrong technique produces an incorrect result.



import { LurchSymbol, Application } from './src/index.js'

// Construct the term that should appear on each side:
const term = LogicConcept.fromPutdown( '(- (* 6 x) 5)' )[0]

// The following step seems correct, but tries to place the same exact term
// object on both sides of an equation, which is two distinct places in an LC
// tree, and will not work as expected:
let equation = new Application( new LurchSymbol( '=' ), term, term )

// The (surprising and unintended) result:
console.log( equation.toPutdown() )
// Console output:
(= (- (* 6 x) 5))


The correct way:



equation = new Application( new LurchSymbol( '=' ), term, term.copy() )
console.log( equation.toPutdown() )
// Console output:
(= (- (* 6 x) 5) (- (* 6 x) 5))


Removing and replacing LCs

To remove an LC from a hierarchy, simply call its remove() method. Note that this will alter its parent (and, indirectly, grandparent, etc.) LC by removing one of its descendants.



console.log( 'Before:', equation.toPutdown() ) // See equation, above.
term.remove() // Recall that term is the left hand side of the equation.
console.log( 'After:', equation.toPutdown() )
// Console output:
Before: (= (- (* 6 x) 5) (- (* 6 x) 5))
After: (= (- (* 6 x) 5))


One can remove an LC and replace it with a different one all in a single operation, as follows.



console.log( 'Before:', env.toPutdown() )
env.child( 4 ).replaceWith( pi.copy() )
console.log( 'After:', env.toPutdown() )
// Console output:
Before: { YO :A B D :E F }
After: { YO :A B D pi F }


If we had written pi instead of pi.copy(), it would have removed the symbol pi from its original place in the famous equation from earlier, rendering that equation not false so much as syntactically invalid.

Two important relations: earlier/later and accessibility

One very important relation among nodes in an LC tree is accessibility. (See that link for the official explanation of the concept.) One can test it in a variety of equivalent ways:



const c1 = env.firstChild()
const c2 = env.lastChild()
console.log( c1.isAccessibleTo( c2 ) )
console.log( c2.isInTheScopeOf( c1 ) ) // equivalent to previous line
// Console output:
true
true


You can also get a list of all the things accessible to (or in the scope of) a given LC.



console.log( c2.accessibles().map( x => x.toPutdown() ) )
console.log( c1.scope().map( x => x.toPutdown() ) )
// Console output:
[ 'pi', 'D', 'B', ':A', 'YO' ]
[ 'YO', ':A', 'B', 'D', 'pi', 'F' ]


The second very important relationship is whether one node appears earlier than another in (a pre-order traversal of) the LC tree in which both sit.



console.log( c1.isEarlierThan( c2 ) )
console.log( c1.isLaterThan( env ) ) // children are later than parents
// Console output:
true
true


You can also ask for the next or previous node in a pre-order tree traversal. This sometimes coincides with being the next or previous sibling, but not always. Consider the following example.



let temp = famous
while ( temp ) {
    console.log( temp.toPutdown() )
    temp = temp.nextInTree()
}
// Console output:
(= (+ (^ e (* i pi)) 1) 0)
=
(+ (^ e (* i pi)) 1)
+
(^ e (* i pi))
^
e
(* i pi)
*
i
pi
1
0